/*
  Javascript State Machine Library - https://github.com/jakesgordon/javascript-state-machine
  Copyright (c) 2012, 2013, 2014, 2015, Jake Gordon and contributors
  Released under the MIT license - https://github.com/jakesgordon/javascript-state-machine/blob/master/LICENSE
*/

(function () {

    var StateMachine = {
  
      //---------------------------------------------------------------------------
  
      VERSION: "2.4.0",
  
      //---------------------------------------------------------------------------
  
      Result: {
        SUCCEEDED:    1, // the event transitioned successfully from one state to another
        NOTRANSITION: 2, // the event was successfull but no state transition was necessary
        CANCELLED:    3, // the event was cancelled by the caller in a beforeEvent callback
        PENDING:      4  // the event is asynchronous and the caller is in control of when the transition occurs
      },
  
      Error: {
        INVALID_TRANSITION: 100, // caller tried to fire an event that was innapropriate in the current state
        PENDING_TRANSITION: 200, // caller tried to fire an event while an async transition was still pending
        INVALID_CALLBACK:   300 // caller provided callback function threw an exception
      },
  
      WILDCARD: '*',
      ASYNC: 'async',
  
      //---------------------------------------------------------------------------
  
      create: function(cfg, target) {
  
        var initial      = (typeof cfg.initial == 'string') ? { state: cfg.initial } : cfg.initial; // allow for a simple string, or an object with { state: 'foo', event: 'setup', defer: true|false }
        var terminal     = cfg.terminal || cfg['final'];
        var fsm          = target || cfg.target  || {};
        var events       = cfg.events || [];
        var callbacks    = cfg.callbacks || {};
        var map          = {}; // track state transitions allowed for an event { event: { from: [ to ] } }
        var transitions  = {}; // track events allowed from a state            { state: [ event ] }
  
        var add = function(e) {
          var from = Array.isArray(e.from) ? e.from : (e.from ? [e.from] : [StateMachine.WILDCARD]); // allow 'wildcard' transition if 'from' is not specified
          map[e.name] = map[e.name] || {};
          for (var n = 0 ; n < from.length ; n++) {
            transitions[from[n]] = transitions[from[n]] || [];
            transitions[from[n]].push(e.name);
  
            map[e.name][from[n]] = e.to || from[n]; // allow no-op transition if 'to' is not specified
          }
          if (e.to)
            transitions[e.to] = transitions[e.to] || [];
        };
  
        if (initial) {
          initial.event = initial.event || 'startup';
          add({ name: initial.event, from: 'none', to: initial.state });
        }
  
        for(var n = 0 ; n < events.length ; n++)
          add(events[n]);
  
        for(var name in map) {
          if (map.hasOwnProperty(name))
            fsm[name] = StateMachine.buildEvent(name, map[name]);
        }
  
        for(var name in callbacks) {
          if (callbacks.hasOwnProperty(name))
            fsm[name] = callbacks[name]
        }
  
        fsm.current     = 'none';
        fsm.is          = function(state) { return Array.isArray(state) ? (state.indexOf(this.current) >= 0) : (this.current === state); };
        fsm.can         = function(event) { return !this.transition && (map[event] !== undefined) && (map[event].hasOwnProperty(this.current) || map[event].hasOwnProperty(StateMachine.WILDCARD)); }
        fsm.cannot      = function(event) { return !this.can(event); };
        fsm.transitions = function()      { return (transitions[this.current] || []).concat(transitions[StateMachine.WILDCARD] || []); };
        fsm.isFinished  = function()      { return this.is(terminal); };
        fsm.error       = cfg.error || function(name, from, to, args, error, msg, e) { throw e || msg; }; // default behavior when something unexpected happens is to throw an exception, but caller can override this behavior if desired (see github issue #3 and #17)
        fsm.states      = function() { return Object.keys(transitions).sort() };
  
        if (initial && !initial.defer)
          fsm[initial.event]();
  
        return fsm;
  
      },
  
      //===========================================================================
  
      doCallback: function(fsm, func, name, from, to, args) {
        if (func) {
          try {
            return func.apply(fsm, [name, from, to].concat(args));
          }
          catch(e) {
            return fsm.error(name, from, to, args, StateMachine.Error.INVALID_CALLBACK, "an exception occurred in a caller-provided callback function", e);
          }
        }
      },
  
      beforeAnyEvent:  function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onbeforeevent'],                       name, from, to, args); },
      afterAnyEvent:   function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onafterevent'] || fsm['onevent'],      name, from, to, args); },
      leaveAnyState:   function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onleavestate'],                        name, from, to, args); },
      enterAnyState:   function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onenterstate'] || fsm['onstate'],      name, from, to, args); },
      changeState:     function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onchangestate'],                       name, from, to, args); },
  
      beforeThisEvent: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onbefore' + name],                     name, from, to, args); },
      afterThisEvent:  function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onafter'  + name] || fsm['on' + name], name, from, to, args); },
      leaveThisState:  function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onleave'  + from],                     name, from, to, args); },
      enterThisState:  function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onenter'  + to]   || fsm['on' + to],   name, from, to, args); },
  
      beforeEvent: function(fsm, name, from, to, args) {
        if ((false === StateMachine.beforeThisEvent(fsm, name, from, to, args)) ||
            (false === StateMachine.beforeAnyEvent( fsm, name, from, to, args)))
          return false;
      },
  
      afterEvent: function(fsm, name, from, to, args) {
        StateMachine.afterThisEvent(fsm, name, from, to, args);
        StateMachine.afterAnyEvent( fsm, name, from, to, args);
      },
  
      leaveState: function(fsm, name, from, to, args) {
        var specific = StateMachine.leaveThisState(fsm, name, from, to, args),
            general  = StateMachine.leaveAnyState( fsm, name, from, to, args);
        if ((false === specific) || (false === general))
          return false;
        else if ((StateMachine.ASYNC === specific) || (StateMachine.ASYNC === general))
          return StateMachine.ASYNC;
      },
  
      enterState: function(fsm, name, from, to, args) {
        StateMachine.enterThisState(fsm, name, from, to, args);
        StateMachine.enterAnyState( fsm, name, from, to, args);
      },
  
      //===========================================================================
  
      buildEvent: function(name, map) {
        return function() {
  
          var from  = this.current;
          var to    = map[from] || (map[StateMachine.WILDCARD] != StateMachine.WILDCARD ? map[StateMachine.WILDCARD] : from) || from;
          var args  = Array.prototype.slice.call(arguments); // turn arguments into pure array
  
          if (this.transition)
            return this.error(name, from, to, args, StateMachine.Error.PENDING_TRANSITION, "event " + name + " inappropriate because previous transition did not complete");
  
          if (this.cannot(name))
            return this.error(name, from, to, args, StateMachine.Error.INVALID_TRANSITION, "event " + name + " inappropriate in current state " + this.current);
  
          if (false === StateMachine.beforeEvent(this, name, from, to, args))
            return StateMachine.Result.CANCELLED;
  
          if (from === to) {
            StateMachine.afterEvent(this, name, from, to, args);
            return StateMachine.Result.NOTRANSITION;
          }
  
          // prepare a transition method for use EITHER lower down, or by caller if they want an async transition (indicated by an ASYNC return value from leaveState)
          var fsm = this;
          this.transition = function() {
            fsm.transition = null; // this method should only ever be called once
            fsm.current = to;
            StateMachine.enterState( fsm, name, from, to, args);
            StateMachine.changeState(fsm, name, from, to, args);
            StateMachine.afterEvent( fsm, name, from, to, args);
            return StateMachine.Result.SUCCEEDED;
          };
          this.transition.cancel = function() { // provide a way for caller to cancel async transition if desired (issue #22)
            fsm.transition = null;
            StateMachine.afterEvent(fsm, name, from, to, args);
          }
  
          var leave = StateMachine.leaveState(this, name, from, to, args);
          if (false === leave) {
            this.transition = null;
            return StateMachine.Result.CANCELLED;
          }
          else if (StateMachine.ASYNC === leave) {
            return StateMachine.Result.PENDING;
          }
          else {
            if (this.transition) // need to check in case user manually called transition() but forgot to return StateMachine.ASYNC
              return this.transition();
          }
  
        };
      }
  
    }; // StateMachine
  
    //===========================================================================
  
    //======
    // NODE
    //======
    if (typeof exports !== 'undefined') {
      if (typeof module !== 'undefined' && module.exports) {
        exports = module.exports = StateMachine;
      }
      exports.StateMachine = StateMachine;
    }
    //============
    // AMD/REQUIRE
    //============
    else if (typeof define === 'function' && define.amd) {
      define(function(require) { return StateMachine; });
    }
    //========
    // BROWSER
    //========
    else if (typeof window !== 'undefined') {
      window.StateMachine = StateMachine;
    }
    //===========
    // WEB WORKER
    //===========
    else if (typeof self !== 'undefined') {
      self.StateMachine = StateMachine;
    }
  
  }());